/*
 * Copyright 2004-2005 The Apache Software Foundation or its licensors,
 *                     as applicable.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.extension;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Value;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.extension.configuration.ItemConfiguration;
import org.apache.jackrabbit.extension.configuration.RepositoryConfiguration;

/**
 * The <code>ExtensionDescriptor</code> class implements a descriptor for an
 * extension defined in a repository node with mixin node type
 * <code>rep:extension</code>.
 * <p>
 * Two instances of this class are considered equal if they are the same
 * instance or if they are of the same extension type and if their extension
 * names are equal.
 * <p>
 * This class implements the <code>Comparable</code> interface defining an order
 * amongst two instances of this class according to the extension type
 * identification and the extension name. See {@link #compareTo(Object)}.
 *
 * @author Felix Meschberger
 * @version $Rev:$, $Date$
 *
 * @see org.apache.jackrabbit.extension.ExtensionType
 * @see org.apache.jackrabbit.extension.ExtensionManager
 */
public class ExtensionDescriptor implements Comparable {

    /** default log */
    private static final Log log = LogFactory.getLog(ExtensionDescriptor.class);

    /**
     * The name of the property containing the extension type identification
     * (value is "rep:id").
     * This is a mandatory property of an extension node.
     */
    public static final String PROP_REP_ID = "rep:id";

    /**
     * The name of the property containing the extension name (value is
     * "rep:name").
     * This is a mandatory property of an extension node.
     */
    public static final String PROP_REP_NAME = "rep:name";

    /**
     * The name of the property containing the fully qualified name of a class
     * implementing the extension (value is "rep:class").
     * This is an optional property of the extension node.
     */
    public static final String PROP_REP_CLASS = "rep:class";

    /**
     * The name of the multivalue property containing the class path providing
     * the extension class(es) (value is "rep:classpath").
     * This is an optional property of the extension node.
     */
    public static final String PROP_REP_CLASSPATH = "rep:classpath";

    /**
     * The name of the property containing the fully qualified name of a class
     * implementing the <code>org.apache.commons.configuration.Configuration</code>
     * interface (value is "rep:configurationClass").
     * This is an optional property of the extension node.
     */
    public static final String PROP_REP_CONFIGURATION_CLASS =
        "rep:configurationClass";

    /**
     * The name of the child node containing the configuration for this
     * extension (value is "rep:configuration").
     * This is an optional child node of the extension node.
     */
    public static final String NODE_REP_CONFIGURATION = "rep:configuration";

    /**
     * The {@link ExtensionType} to which this extension belongs.
     * @see #getExtensionType
     */
    private final ExtensionType type;

    /**
     * The <code>Node</code> from which this descriptor has been loaded.
     * @see #getNode()
     */
    private final Node node;

    /**
     * The extension type identification read from the {@link #PROP_REP_ID}
     * property of the node describing the extension.
     * @see #getId()
     */
    private final String id;

    /**
     * The extension name read from the {@link #PROP_REP_NAME} property of the
     * node describing the extension.
     * @see #getName()()
     */
    private final String name;

    /**
     * The fully qualified name of the class implementing the extension or
     * <code>null</code> if none is defined. The value of this field is read
     * from the {@link #PROP_REP_CLASS} property of the node describing the
     * extension.
     * @see #getClassName()
     */
    private final String className;

    /**
     * The classpath to configure on the extension type's class loader to load
     * and use this extension or <code>null</code> if none is defined. The value
     * of this field is read from the {@link #PROP_REP_CLASSPATH} property of
     * the node describing the extension.
     * @see #getClassPath()
     */
    private final String[] classPath;

    /**
     * The fully qualified name of the class implementing the Apache Jakarta
     * Commons <code>Configuration</code> interface or <code>null</code> if
     * none is defined. The value of this field is read from the
     * {@link #PROP_REP_CONFIGURATION_CLASS} property of the node describing the
     * extension.
     * @see #getConfigurationClassName()
     * @see #getConfiguration()
     * @see #getConfigurationNode()
     */
    private final String configurationClassName;

    /**
     * The absolute path of the {@link #node} from which this descriptor has
     * been loaded.
     * @see #getNodePath();
     */
    private String nodePath;

    /**
     * The extension instance created for this descriptor by the
     * {@link #getExtension()} method or <code>null</code> if none has been
     * created yet.
     * @see #getExtension()
     */
    private Object extension;

    /**
     * The configuration object created for this descriptor by the
     * {@link #getConfiguration()} method or <code>null</code> if none has been
     * created yet.
     * @see #getConfiguration()
     */
    private Configuration configuration;

    /**
     * Creates an instance of this class loading the definition from the given
     * <code>extensionNode</code>.
     * <p>
     * This method does not check whether the node is of the correct type but
     * merely accesses the properties required to exist and tries to access
     * optional properties. If an error occurrs accessing the properties,
     * an <code>ExtensionException</code> is thrown with the cause set.
     *
     * @param type The {@link ExtensionType} having loaded this extension
     *      object.
     * @param extensionNode The <code>Node</code> containing the extension
     *      description.
     *
     * @throws ExtensionException If an error occurrs reading the extension
     *      description from the node.
     */
    /* package */ ExtensionDescriptor(ExtensionType type, Node extensionNode)
            throws ExtensionException {

        this.type = type;
        node = extensionNode;

        try {
            // required data
            id = getPropertyOrNull(extensionNode, PROP_REP_ID);
            name = getPropertyOrNull(extensionNode, PROP_REP_NAME);
            if (id == null || name == null) {
                throw new ExtensionException("Missing id or name property");
            }

            // optional class, classpath and configuration class
            className = getPropertyOrNull(extensionNode, PROP_REP_CLASS);
            classPath = getPropertiesOrNull(extensionNode, PROP_REP_CLASSPATH);
            configurationClassName =
               getPropertyOrNull(extensionNode, PROP_REP_CONFIGURATION_CLASS);
        } catch (RepositoryException re) {
            throw new ExtensionException("Cannot load extension", re);
        }
    }

    /**
     * Returns the {@link ExtensionType} which has loaded this extension.
     */
    private ExtensionType getExtensionType() {
        return type;
    }

    /**
     * Returns the <code>Node</code> from which this extension has been loaded.
     * Any modification to the node returned will only be active the next
     * time an instance of this class is created from the node.
     */
    public final Node getNode() {
        return node;
    }

    /**
     * Returns the absolute path of the <code>Node</code> from which this
     * extension has been loaded.
     */
    public final String getNodePath() {
        if (nodePath == null) {
            try {
                nodePath = getNode().getPath();
            } catch (RepositoryException re) {
                log.warn("Cannot get the path of the extension node", re);
                nodePath = getNode().toString();
            }
        }

        return nodePath;
    }

    /**
     * Returns the identification of the extension type implemented by this
     * extension.
     */
    public final String getId() {
        return id;
    }

    /**
     * Returns the name of this extension.
     */
    public final String getName() {
        return name;
    }

    /**
     * Returns the fully qualified name of the class implementing this extension
     * or <code>null</code> if none is configured in the extension descriptor
     * node.
     */
    public final String getClassName() {
        return className;
    }

    /**
     * Returns the extension class path or <code>null</code> if none has been
     * configured in the extension descriptor. Note that an empty array is
     * never returned by this method.
     */
    public final String[] getClassPath() {
        return classPath;
    }

    /**
     * Returns the fully qualified name of the extensions configuration class
     * or <code>null</code> if none is configured in the extension's node.
     * @see #getConfiguration()
     * @see #getConfigurationNode()
     */
    public final String getConfigurationClassName() {
        return configurationClassName;
    }

    //---------- Instantiation support ----------------------------------------

    /**
     * Returns the class loader to be used to load the extension object and the
     * configuration for the extension described by this descriptor.
     */
    public ClassLoader getExtensionLoader() {
        return getExtensionType().getClassLoader(this);
    }

    /**
     * Creates an instance of the extension class defined by this descriptor.
     * <p>
     * If the descriptor contains a classpath specification, the class loader of
     * the extension type to which the extension belongs, is configured with the
     * additional classpath.
     * <p>
     * The extension class must provide either of two constructors for it to be
     * instantiated by this method:
     * <ol>
     * <li>If a public constructor taking an instance of
     * {@link ExtensionDescriptor} is available, that constructor is used to
     * create the extension instance.</il>
     * <li>Otherwise if a public default constructor taking no paramaters at
     * all is available, that constructor is used to create the extension
     * instance. In this case it is the responsibility of the application to
     * provide the extension instance with more information if required.</li>
     * </ol>
     * <p>
     * If neither constructor is available in the class, this method fails with
     * an {@link ExtensionException}.
     * <p>
     * If the class provides a public method taking a single parameter of
     * type <code>ExtensionDescriptor</code>, that method is called with this
     * instance as the parameter value. This allows for parameterless default
     * constructors in the extension classes while still getting the extension
     * descriptor.
     * <p>
     * If no class has been defined for this extension, an
     * <code>IllegalArgumentException</code> is thrown.
     *
     * @return The instance created for this extension.
     *
     * @throws IllegalArgumentException if no extension class specification is
     *      available in this extension descriptor.
     * @throws ExtensionException if the extension class has no suitable
     *      constructor or if an error occurrs loading or instantiating the
     *      class.
     */
    public Object getExtension() throws ExtensionException {

        // immediately return the extension, if it is already defined
        if (extension != null) {
            return extension;
        }
        // otherwise, we have to instantiate

        // fail if there is no class name in the descriptor
        if (getClassName() == null) {
            throw new IllegalArgumentException("Descriptor has no class definition");
        }

        try {
            log.debug("Loading class " + getClassName());
            Class clazz = getExtensionLoader().loadClass(getClassName());
            Object extension = instantiate(clazz);
            setDescriptor(extension);
            return extension;

        } catch (Exception e) {
            throw new ExtensionException("Cannot instantiate extension " +
                getClassName(), e);
        }
    }

    /**
     * Returns the node containing the configuration of this extension. If the
     * extension's node has a child node <code>rep:configuration</code>, that
     * child node is returned, otherwise the extension's node is returned.
     *
     * @return The configuration node of this extension.
     */
    public Node getConfigurationNode() {
        Node node = getNode();

        try {
            if (node.hasNode(NODE_REP_CONFIGURATION)) {
                return node.getNode(NODE_REP_CONFIGURATION);
            }
        } catch (RepositoryException re) {
            log.warn("Cannot check or access configuration node " +
                NODE_REP_CONFIGURATION + ". Using extension node", re);
        }

        return node;
    }

    /**
     * Returns the <code>Configuration</code> object used to configure this
     * extension.
     * <p>
     * If the extension descriptor does not contain the fully qualified name of
     * a configuration class, this method returns an instance of the
     * {@link ItemConfiguration} class loaded from the extension's node.
     * <p>
     * Otherwise the named class is loaded through the extensions class loader
     * (see {@link #getExtensionLoader()}) and instantiated. A class to be used
     * like this must implement the <code>Configuration</code> interface and
     * provide a public default constructor. If any of the requirements is not
     * met by the configured class, this method throws an exception.
     * <p>
     * If the configured class implements the {@link RepositoryConfiguration}
     * interface, the configuration is configured with the extension's node
     * and loaded.
     * <p>
     * The main use of this method is for the extension class itself to
     * configure itself. Another use may be for an administrative application
     * to update configuration and optionally store it back.
     *
     * @return The <code>Configuration</code> object used to configured this
     *      extension.
     *
     * @throws ExtensionException If the configuration class has no public
     *      default constructor or if the configuration class is not an
     *      implementation of the <code>Configuration</code> interface or if an
     *      error occurrs loading or instantiating the configuration class.
     */
    public Configuration getConfiguration() throws ExtensionException {
        // immediately return the configuration, if it is already defined
        if (configuration != null) {
            return configuration;
        }
        // otherwise, we have to instantiate

        // use a default configuration if no specific class defined
        if (getConfigurationClassName() == null) {
            log.debug("No configurationClass setting, using ItemConfiguration");
            try {
                return new ItemConfiguration(getConfigurationNode());
            } catch (ConfigurationException ce) {
                throw new ExtensionException(
                    "Cannot load ItemConfiguration from " + getNodePath());
            }
        }

        try {
            log.debug("Loading class " + getConfigurationClassName());
            Class clazz =
                getExtensionLoader().loadClass(getConfigurationClassName());

            // create an instance using the one taking an extension descriptor
            // if available otherwise use the default constructor
            log.debug("Creating configuration object instance");
            Object configObject = clazz.newInstance();
            if (!(configObject instanceof Configuration)) {
                throw new ExtensionException("Configuration class " +
                    getClassName() +
                    " does not implement Configuration interface");
            }

            // load the repository configuration from the extension node
            if (configObject instanceof RepositoryConfiguration) {
                RepositoryConfiguration repoConfig =
                    (RepositoryConfiguration) configObject;
                repoConfig.setNode(getConfigurationNode());
                repoConfig.load();
            }

            configuration = (Configuration) configObject;
            return configuration;

        } catch (Exception e) {
            throw new ExtensionException("Cannot instantiate extension " +
                getClassName(), e);
        }
    }

    //---------- Comparable interface -----------------------------------------

    /**
     * Compares this object with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.

     * @param obj the Object to be compared, which must be an instance of this
     *      class.
     *
     * @return a negative integer, zero, or a positive integer as this
     *      descriptor is less than, equal to, or greater than the specified
     *      descriptor.
     *
     * @throws NullPointerException if <code>obj</code> is <code>null</code>.
     * @throws ClassCastException if <code>obj</code> is not an
     *      <code>ExtensionDescriptor</code>.
     */
    public int compareTo(Object obj) {
        // throws documented ClassCastException
        ExtensionDescriptor other = (ExtensionDescriptor) obj;

        // check the order amongst the id and return if not equal
        int idOrder = id.compareTo(other.id);
        if (idOrder != 0) {
            return idOrder;
        }

        // id's are the same, so return order amongst names
        return name.compareTo(other.name);
    }

    //---------- Object overwrite ---------------------------------------------

    /**
     * Returns a combined hash code of the {@link #getId() type identification}
     * and the {@link #getName() name} of this extension as this extension's
     * hash code.
     */
    public int hashCode() {
        return id.hashCode() + 17 * name.hashCode();
    }

    /**
     * Returns <code>true</code> if <code>obj</code> is the same as this or
     * if it is a <code>ExtensionDescriptor</code> whose type identification
     * and name equals the type identification and name of this extension.
     */
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (obj instanceof ExtensionDescriptor) {
            ExtensionDescriptor other = (ExtensionDescriptor) obj;
            return id.equals(other.id) && name.equals(other.name);
        } else {
            return false;
        }
    }

    /**
     * Returns a string representation of this extension descriptor which
     * contains the extension type identification and the extension name.
     */
    public String toString() {
        return "Extension " + id + ":" + name;
    }

    //---------- innternal ----------------------------------------------------

    /**
     * Returns the value of the {@link #PROP_REP_NAME} property of the
     * <code>extensionNode</code> or <code>null</code> if no such property
     * exists.
     *
     * @param extensionNode The <code>Node</code> whose extension name property
     *      value is to bereturned.
     *
     * @throws RepositoryException if an error occurrs accessing the extension
     *      name property.
     */
    /* package */ static String getExtensionName(Node extensionNode)
            throws RepositoryException {
        return getPropertyOrNull(extensionNode, PROP_REP_NAME);
    }

    /**
     * Returns the string value of the named (single-value) property of the
     * node or <code>null</code> if the the property does not exists or its
     * value is empty.
     *
     * @param node The <code>Node</code> containing the named property.
     * @param property The name of the property to reutrn.
     *
     * @return The property's string value or <code>null</code> if the property
     *      does not exist or is empty.
     *
     * @throws RepositoryException If an error occurrs accesing the node or
     *      property.
     */
    private static String getPropertyOrNull(Node node, String property)
            throws RepositoryException {
        if (node.hasProperty(property)) {
            String value = node.getProperty(property).getString();
            return (value == null || value.length() == 0) ? null : value;
        }

        return null;
    }

    /**
     * Returns the string values of the named (multi-valued) property of the
     * node or <code>null</code> if the the property does not exists or its
     * value is empty.
     *
     * @param node The <code>Node</code> containing the named property.
     * @param property The name of the property to reutrn.
     *
     * @return A string array containing the string representations of the
     *      property's values or <code>null</code> if the property does not
     *      exist or is empty.
     *
     * @throws RepositoryException If an error occurrs accesing the node or
     *      property.
     */
    private static String[] getPropertiesOrNull(Node node, String property)
            throws RepositoryException {

        if (node.hasProperty(property)) {
            Value[] clsPath = node.getProperty(property).getValues();
            if (clsPath != null && clsPath.length >= 0) {
                List pathList = new ArrayList();
                for (int i=0; i < clsPath.length; i++) {
                    String pathEntry = clsPath[i].getString().trim();

                    // ignore empty or existing path entry
                    if (pathEntry.length() == 0 ||
                            pathList.contains(pathEntry)) {
                        continue;
                    }

                    // new class path entry, add
                    pathList.add(pathEntry);
                }

                if (pathList.size() > 0) {
                    return (String[]) pathList.toArray(new String[pathList.size()]);
                }
            }
        }

        return null;
    }

    /**
     * Creates an instance of the given <code>clazz</code>. If the class has
     * a public constructor taking a single parameter of type
     * <code>ExtensionDescriptor</code> that constructor is used to create the
     * instance. Otherwise the public default constructor is used if available.
     * If none of both is available or if an error occurrs creating the instance
     * a <code>ExtensionException</code> is thrown.
     *
     * @param clazz The <code>Class</code> to instantiate.
     *
     * @return The instance created.
     *
     * @throws ExtensionException If an error occurrs instantiating the class.
     *      If instantiation failed due to an exception while calling the
     *      constructor, the causing exception is available as the cause of
     *      the exception.
     */
    private Object instantiate(Class clazz) throws ExtensionException {
        // find constructors (taking descriptor and default)
        Constructor defaultConstr = null;
        Constructor descrConstr = null;
        Constructor[] constructors = clazz.getConstructors();
        for (int i=0; i < constructors.length; i++) {
            Class parms[] = constructors[i].getParameterTypes();
            if (parms.length == 0) {
                defaultConstr = constructors[i];
            } else if (parms.length == 1 && parms[i].equals(getClass())) {
                descrConstr = constructors[i];
            }
        }

        try {
            // create an instance using the one taking an extension descriptor
            // if available otherwise use the default constructor
            if (descrConstr != null) {
                log.debug("Creating instance with descriptor " + this);
                return descrConstr.newInstance(new Object[]{ this });
            } else if (defaultConstr != null) {
                log.debug("Creating default instance without descriptor");
                return defaultConstr.newInstance(null);
            } else {
                throw new ExtensionException("No suitable constructor found " +
                        "to instantiate " +  getClassName());
            }
        } catch (InstantiationException ie) {
            throw new ExtensionException(
                "Cannot instantiate " + getClassName(), ie);
        } catch (IllegalAccessException iae) {
            throw new ExtensionException("Cannot access constructor of "
                + getClassName(), iae);
        } catch (InvocationTargetException ite) {
            throw new ExtensionException("Error while instantiating "
                + getClassName(), ite);
        }
    }

    /**
     * Calls a method taking a single parameter of type
     * <code>ExtensionDescriptor</code> to provide the extension descriptor to
     * the extension loaded.
     * <p>
     * If an error occurrs calling a method found, an WARN message is logged and
     * other methods according to the required signature are looked for. If no
     * suitable method can be found, an INFO method is logged and the extension
     * could not be provided with the extension descriptor.
     *
     * @param extension The extension to provide witch the extension descriptor.
     */
    private void setDescriptor(Object extension) {
        Method[] methods = extension.getClass().getMethods();
        for (int i=0; i < methods.length; i++) {
            Class[] parTypes = methods[i].getParameterTypes();
            if (parTypes.length == 1 && parTypes[0].equals(getClass())) {
                try {
                    methods[i].invoke(extension, new Object[]{ this });
                    return;
                } catch (Exception ite) {
                    log.warn("setDescriptor: Calling " +
                        extension.getClass().getName() + "." +
                        methods[i].getName() + " failed", ite);
                }
            }
        }

        log.info("setDescriptor: No setter method for ExtensionDescriptor " +
                "found in class " + extension.getClass().getName() +
                " of extension " + getId() + ":" + getName());
    }
}
